using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace UMC.Net
{
    public class Socks5
    {
        private readonly string _proxyHost;
        private readonly int _proxyPort;
        private readonly StringValue _username = StringValue.Empty;
        private readonly StringValue _password = StringValue.Empty;

        public Socks5(string proxyHost, int proxyPort, StringValue username, StringValue password)
        {
            _proxyHost = proxyHost;
            _proxyPort = proxyPort;
            this._password = password;
            this._username = username;
        }
        public Socks5(string proxyHost, int proxyPort, String username, String password)
        {
            _proxyHost = proxyHost;
            _proxyPort = proxyPort;
            this._password = new StringValue(password);
            this._username = new StringValue(username);
        }
        IPAddress _proxyIp;
        public Socks5(IPAddress proxyHost, int proxyPort, StringValue username, StringValue password)
        {
            _proxyIp = proxyHost;
            _proxyPort = proxyPort;
            this._password = password;
            this._username = username;
        }
        public StringValue Username => _username;
        public StringValue Password => _password;
        public String Host => _proxyHost;
        public int Port => _proxyPort;
        public Socks5(string proxyHost, int proxyPort)
        {
            _proxyHost = proxyHost;
            _proxyPort = proxyPort;
        }

        // public async Task<Socket> ConnectAsync(IPAddress destinationHost, int destinationPort, CancellationToken cancellationToken = default)
        // {
        //     var client = await NetProxy.Connect(_proxyHost, _proxyPort, 0, cancellationToken);

        // }

        public async Task<Socket> ConnectAsync(StringValue destinationHost, int destinationPort, int timeout, CancellationToken cancellationToken = default)
        {
            var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
            {
                ReceiveTimeout = timeout,
                SendTimeout = timeout,
                ExclusiveAddressUse = true,
                LingerState = new LingerOption(true, 10),
                NoDelay = true
            };
            await client.ConnectAsync(new IPEndPoint(_proxyIp ?? (await NetProxy.DNSToIPAsync(_proxyHost)).FirstOrDefault(), _proxyPort), cancellationToken);//, cancellationTokenSource.Token);

            var buffers = System.Buffers.ArrayPool<byte>.Shared.Rent(150);
            try
            {
                buffers[0] = 0x05;
                buffers[1] = 0x01;
                if (_username.IsEmpty())
                {
                    buffers[2] = 0x00;
                }
                else
                {

                    buffers[2] = 0x02;
                }

                await client.SendAsync(new ArraySegment<byte>(buffers, 0, 3), cancellationToken);
                await client.ReceiveAsync(new ArraySegment<byte>(buffers, 0, 2), cancellationToken);
                switch (buffers[1])
                {
                    case 0x00:
                        break;
                    case 0x02:
                        buffers[0] = 0x01;
                        int size = _username.Length;
                        buffers[1] = (byte)size;
                        _username.Span.CopyTo(buffers.AsSpan(2));
                        size += 2;
                        int pw = _password.Length;
                        buffers[size] = (byte)pw;
                        size++;

                        _password.Span.CopyTo(buffers.AsSpan(size));
                        size += pw;
                        await client.SendAsync(new ArraySegment<byte>(buffers, 0, size), cancellationToken);
                        await client.ReceiveAsync(new ArraySegment<byte>(buffers, 0, 2), cancellationToken);
                        if (buffers[1] != 0x00)
                        {
                            throw new Exception("SOCKS5 authentication failed.");
                        }
                        break;
                    default:
                        throw new Exception("SOCKS5 authentication failed.");
                        // break;
                }

                buffers[0] = 0x05; // SOCKS version
                buffers[1] = 0x01; // CONNECT command
                buffers[2] = 0x00; // Reserved
                buffers[3] = 0x03; // Address type (domain name)
                                   // request[4] = (byte)addressBytes.Length;
                destinationHost.Span.CopyTo(buffers.AsSpan(5));
                int l = destinationHost.Length;// destinationHost.WriteBytes(buffers, 5);
                buffers[4] = (byte)l;
                // Array.Copy(addressBytes, 0, request, 5, addressBytes.Length);
                buffers[5 + l] = (byte)(destinationPort >> 8);
                buffers[6 + l] = (byte)destinationPort;
                await client.SendAsync(new ArraySegment<byte>(buffers, 0, l + 7), cancellationToken);//, SocketFlags.None);

                // var responseHeader = new byte[4];
                await client.ReceiveAsync(new ArraySegment<byte>(buffers, 0, 4), cancellationToken);//, SocketFlags.None);

                if (buffers[1] != 0x00)
                {
                    throw new Exception("SOCKS5 connection failed.");
                }
                int addressLength;
                var addressType = buffers[3];
                switch (addressType)
                {
                    case 0x01:
                        addressLength = 4;
                        break;
                    case 0x04:
                        addressLength = 16;
                        break;
                    case 0x03:
                        await client.ReceiveAsync(new ArraySegment<byte>(buffers, 0, 1), cancellationToken);//, SocketFlags.None);
                        addressLength = buffers[0];
                        break;
                    default:
                        throw new Exception("Unknown address type.");
                        // break;
                }

                await client.ReceiveAsync(new ArraySegment<byte>(buffers, 0, addressLength + 2), cancellationToken);

                return client;
            }
            finally
            {
                System.Buffers.ArrayPool<byte>.Shared.Return(buffers);
            }
        }
    }
}
